iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Build on Google AI

30 天用 Google AI 打造台灣防災速報 App系列 第 18 篇

Day 18|不寫 Kotlin 也不寫 Swift:Flutter 同一套程式碼做出 Android 與 iOS App

  • 分享至 

  • xImage
  •  

系列:30 天用 Google AI 打造臺灣防災速報 App(Day 18/30)

後端上線了,接著做 App。Android 原生要寫 Kotlin,iOS 原生要寫 Swift(兩個平台各自的官方開發語言),一個人維護兩套程式碼太吃力。這個系列用 Flutter:Google 的跨平台 App 框架,用 Dart 語言寫一次,就能建置成 Android、iOS 和網頁版。

今天講環境建置、專案結構,以及實際跑過 Android、iOS 之後要注意的地方。

安裝與檢查

在 macOS 上用 Homebrew(套件管理工具)安裝:

brew install --cask flutter
brew install --cask android-studio
flutter doctor -v

flutter doctor 會逐項檢查各平台需要的工具。第一次跑的結果(節錄):

[✓] Flutter (Channel stable, 3.47.5, on macOS 26.7.1 25G313 darwin-arm64, locale zh-Hant-TW)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
[!] Xcode - develop for iOS and macOS (Xcode 27.0)
    ✗ Xcode requires additional components to be installed in order to run.
[✓] Chrome - develop for the web

Android 缺 SDK(開發套件),iOS 缺 Xcode(Apple 的開發工具)的首次安裝元件,只有網頁版可以直接跑。所以先從網頁版開始,其他平台的工具邊做邊補。

建立專案:識別碼一開始就要定好

先建好專案資料夾並切換進去,再建立專案(最後的 . 代表建在目前的資料夾):

mkdir watchtower_app && cd watchtower_app
flutter create --org tw.watchtower --project-name watchtower_app --platforms android,web .

--org 加上專案名稱,組成 App 的識別碼,也就是系統用來辨認 App 的唯一名稱。Android 叫 application ID,iOS 叫 bundle identifier。上面這樣建立,Android 的識別碼是 tw.watchtower.watchtower_app。

這裡要提醒一件事:識別碼最好在建立專案時就決定好。我們上架前才把 Android 和 iOS 都改成 tw.watchtower.app(Android 的 namespace、applicationId 和 MainActivity 的套件路徑一起改),代價比預期大。Firebase 上已註冊的 App 不能改識別碼,只能新增一個 App、重新下載設定檔;App Check 和 API 金鑰的限制也要重設;手機上已安裝的舊版會被當成另一個 App。

專案的 lib/ 整理成這樣:

lib/
  data/          # 資料來源:示警、地震、問答
  screens/       # 各個畫面
  widgets/       # 共用元件(地圖、卡片)
  l10n/          # 多語言文字
  theme.dart     # 配色、字級、間距

畫面只依賴 data/ 裡定義的介面,不直接碰資料庫。換資料來源時只改 data/,畫面不用動。

多語言

Flutter 的多語系要先加兩個套件:flutter_localizations 提供內建元件的多語文字,intl 處理日期、數字的格式。

flutter pub add intl 'flutter_localizations:{"sdk":"flutter"}'

介面上的固定文字放在 ARB 檔(Flutter 的多語系文字格式),中文、英文、日文、韓文各一個檔案:

{
  "@@locale": "zh",
  "appTitle": "防災速報",
  "activeCount": "共 {count} 則",
  "@activeCount": {
    "placeholders": { "count": { "type": "int" } }
  }
}

我們用中文檔當範本,所以要在專案根目錄的 l10n.yaml 指定(Flutter 預設找的是英文檔):

arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart

再在 pubspec.yaml 的 flutter: 區塊加上 generate: true,執行 flutter gen-l10n 就會產生程式碼。MaterialApp(Flutter App 的最外層元件)要設定 supportedLocales 和 localizationsDelegates,畫面才能用 AppLocalizations.of(context).appTitle 取得目前語言的文字。App 預設跟隨手機的語言,也可以手動切換;日文、韓文還沒有請母語者審核,選單上標「Beta」。

示警的分類(例如「水庫放流」)和發布單位(例如「水利署」)是資料裡的值,ARB 檔管不到,所以另外寫一張固定對照表。這些值數量有限,翻一次、審一次就固定,不交給 AI 翻譯。示警內文才由後端用 Gemini 翻譯(Day 13)。

字型:斷網也要顯示得出來

一開始用 google_fonts 套件,字型在 App 執行時才從網路下載。可是防災 App 最需要的時候,往往網路也不穩。所以改成:

  • 英文字型打包進 App(Plus Jakarta Sans、Atkinson Hyperlegible Next,授權是允許免費使用和散布的 OFL)。
  • 中日韓文字用手機的系統字型,不打包,App 不會變大。
  • 網頁版本來就要連網,由 Flutter 自動補下載中文字型。網頁版第一次打開的一兩秒內,少用的字會先顯示成方框。

Android:第一次建置會很久

Android SDK 由 Android Studio 第一次開啟時的設定精靈安裝到 ~/Library/Android/sdk,Flutter 會自己找到。接著建置 APK(Android 的安裝檔):

flutter build apk --debug

第一次建置,Gradle(Android 的建置工具)的工作花了 2,461.3 秒,整個指令 2,463 秒,約 41 分鐘,大部分時間在下載 Gradle。第二次只要 3.6 秒。

啟動模擬器後,用 adb(Android 的除錯工具)安裝,再用 am start 指定要開的畫面(下面是改名後的識別碼;還沒改名的話,換成建立時的 tw.watchtower.watchtower_app):

adb install -r build/app/outputs/flutter-apk/app-debug.apk
adb shell am start -n tw.watchtower.app/.MainActivity

Android 模擬器上的英文首頁

同一套程式碼,還是有平台差異

能建置成各平台,不代表每個平台的行為都一樣。我們遇到兩個只在某個平台出現的問題。

問答只在 Android 失敗。 呼叫後端 ask 函式時指定了回傳型別 Map<String, dynamic>,網頁版正常,Android 每次都失敗。原因是 Android 原生端回傳的型別是 Map<Object?, Object?>,執行時轉型失敗。改成先不指定型別,拿到結果再轉:

final res = await _functions.httpsCallable('ask').call({
  'question': question,
  'lang': locale,
});
final data = Map<String, dynamic>.from(res.data as Map);

滾輪只在網頁版不能用滑鼠拖。 Flutter 的捲動元件預設只接受觸控拖曳,網頁上要另外加入滑鼠和觸控板:

// import 'package:flutter/gestures.dart';  // PointerDeviceKind
ScrollConfiguration(
  behavior: ScrollConfiguration.of(context).copyWith(
    dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad},
  ),
  child: picker,
)

結論很簡單:用跨平台框架,每個平台還是要實際跑過一次。

iOS

用 flutter create --platforms ios --org tw.watchtower . 把 iOS 加進同一個專案,之後把 bundle identifier 改成 tw.watchtower.app。在模擬器上跑之前,要注意三件事:

  1. Xcode 要先完成首次安裝元件:sudo xcodebuild -runFirstLaunch。
  2. 模擬器的執行環境要另外下載:xcodebuild -downloadPlatform iOS,約 8 到 10 GB。
  3. Firebase 的設定檔 GoogleService-Info.plist 要在 Xcode 裡加進 Runner target(iOS App 本體的建置目標),只放進資料夾不會被打包。Flutter 端的 firebase_options.dart 也要加上 iOS 的設定。

另外,App Check(Day 15)在 iOS 正式版用 App Attest(Apple 提供的 App 完整性驗證服務);模擬器上的除錯版要把印出的權杖登錄到 Firebase 主控台,才能通過驗證。

iOS 模擬器上的英文首頁

和上面 Android 那張來自同一個 Flutter 專案,版面和配色相同。

今日小結

  • Flutter 用同一套 Dart 程式碼建置 Android、iOS 和網頁版;flutter doctor 會列出缺的工具。
  • 識別碼在建立專案時就定好,事後改名要重新註冊 Firebase、重設金鑰限制。
  • 畫面只依賴資料介面;分類、發布單位用固定對照表,不交給 AI 翻譯。
  • 英文字型打包進 App,手機上的中日韓文字用系統字型,斷網也能正常顯示。
  • 同一套程式碼仍有平台差異,每個平台都要實際跑過。

明天 Day 19:Firestore 的示警一更新,App 的畫面就跟著變。


上一篇
Day 17|從一段英文描述開始做出 22 張畫面:用 Google Stitch 設計 App,以及要人工檢查的地方
下一篇
Day 19|示警一更新,畫面就跟著變:Flutter 即時監聽 Firestore,以及「沒有資料」和「沒有示警」的差別
系列文
30 天用 Google AI 打造台灣防災速報 App 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言